using System;
using System.Collections;
using System.Globalization;
using System.Text;

namespace Xtreme.Json
{
  [Flags]
  public enum Formatting
  {
    None = 0,
    Indented = 1,
    EnumToString = 2,
    Unescaped = 4
  }

  public static class JsonSerializer
  {
    public static string ToJson<T>(this T instance, Formatting formatting = Formatting.None)
      where T: class
    {
      return instance.ToJson(0, formatting);
    }

    private static string ToJson<T>(this T instance, int indent, Formatting formatting)
      where T : class
    {
      if (instance == null)
      {
        return String.Empty;
      }

      var pretty = formatting.HasFlag(Formatting.Indented);

      var builder = new StringBuilder();
      AppendIndented(0, null, "{", false, builder, false, false);

      bool added = false;
      int nextIndent = indent + (pretty ? 1 : 0);
      foreach (var property in instance.GetType().GetProperties())
      {
        if (!added && pretty)
        {
          builder.AppendLine();
        }

        object value = property.GetValue(instance, null);
        bool isStringValue = value is string;

        string stringify = null;
        if (isStringValue)
        {
          stringify = ((string)value).Trim();
          if (!formatting.HasFlag(Formatting.Unescaped))
          {
            stringify = JsonEscape(stringify);
          }
        }
        else
        {
          Type type = value != null ? value.GetType() : property.PropertyType;

          if (typeof(IDictionary).IsAssignableFrom(type))
          {
            AppendDictionary(nextIndent, property.Name, (IDictionary)value, builder, formatting);
            added = true;
            continue;
          }

          isStringValue = typeof(string).IsAssignableFrom(type);
          if (!isStringValue)
          {
            if (type.IsArray || typeof(IEnumerable).IsAssignableFrom(type))
            {
              AppendArray(nextIndent, property.Name, value, builder, formatting);
              added = true;
              continue;
            }

            stringify = Stringify(value, indent, formatting, type);
          }
        }

        AppendIndented(nextIndent, property.Name, stringify, isStringValue, builder, true, pretty);
        added = true;
      }

      if (added)
      {
        RemoveTrailingComma(indent, builder, formatting);
      }

      AppendIndented(0, null, "}", false, builder, false, false);
      return builder.ToString();
    }

    private static void AppendDictionary(int indent, string name, IDictionary dictionary, StringBuilder builder, Formatting formatting)
    {
      var pretty = formatting.HasFlag(Formatting.Indented);
      AppendIndented(indent, name, "{", false, builder, false, false);

      bool added = false;
      int nextIndent = indent + (pretty ? 1 : 0);
      foreach (DictionaryEntry entry in dictionary)
      {
        if (!added && pretty)
        {
          builder.AppendLine();
        }
        string stringify = Stringify(entry.Value, indent, formatting);
        AppendIndented(nextIndent, entry.Key.ToString(), stringify, false, builder, true, pretty);
        added = true;
      }

      if (added)
      {
        RemoveTrailingComma(indent, builder, formatting);
      }

      AppendIndented(0, null, "}", false, builder, true, pretty);
    }

    private static void AppendArray(int indent, string name, dynamic value, StringBuilder builder, Formatting formatting)
    {
      var pretty = formatting.HasFlag(Formatting.Indented);

      var enumerable = value as IEnumerable;
      if (enumerable == null)
      {
        AppendIndented(indent, name, "[]", false, builder, true, pretty);
        return;
      }

      AppendIndented(indent, name, "[", false, builder, false, false);

      bool added = false;
      bool addNewLine = pretty && !(enumerable.GetType().ElementType()).IsValueType;

      int nextIndent = indent + (pretty ? 1 : 0);
      foreach (dynamic entry in enumerable)
      {
        if (!added && pretty)
        {
          builder.AppendLine();
        }
        string stringify = Stringify(entry, indent, formatting);
        AppendIndented(nextIndent, null, stringify, false, builder, true, addNewLine);
        added = true;
      }

      if (added)
      {
        RemoveTrailingComma(indent, builder, formatting);
      }

      AppendIndented(0, null, "]", false, builder, true, pretty);
    }

    private static string Stringify(dynamic value, int indent, Formatting formatting, Type type = null)
    {
      type = type ?? value.GetType();
      if (type.IsEnum)
      {
        if (formatting.HasFlag(Formatting.EnumToString))
        {
          return String.Format("\"{0}\"", Enum.GetName(type, value ?? 0));
        }
        return ((int)(value ?? 0)).ToString(CultureInfo.InvariantCulture);
      }

      if (type.IsValueType)
      {
        var stringify = (value ?? String.Empty).ToString();
        if (value is Guid || value is bool)
        {
          return String.Format("\"{0}\"", stringify);
        }
        return stringify;
      }

      TypeCode typeCode = Type.GetTypeCode(type);
      if (value != null)
      {
        string stringify = value.ToString().Trim();
        if (stringify == type.FullName)
        {
          int nextIndent = indent + (formatting.HasFlag(Formatting.Indented) ? 1 : 0);
          return JsonSerializer.ToJson(value, nextIndent, formatting);
        }

        if (stringify.StartsWith("{") && stringify.EndsWith("}"))
        {
          return stringify;
        }

        if (!formatting.HasFlag(Formatting.Unescaped))
        {
          stringify = JsonEscape(stringify);
        }
        return String.Format("\"{0}\"", stringify);
      }

      return typeCode == TypeCode.String ? "\"\"" : "{}";
    }

    private static String JsonEscape(String value)
    {
      if (String.IsNullOrEmpty(value))
      {
        return String.Empty;
      }

      int len = value.Length;
      var sb = new StringBuilder(len + 4);

      for (int i = 0; i < len; i += 1)
      {
        char c = value[i];
        switch (c)
        {
          case '/':  //solidus
          case '\\': //reverse solidus
          case '"':  //quotation mark
            sb.Append('\\');
            sb.Append(c);
            break;
          case '\b': //backspace
            sb.Append("\\b");
            break;
          case '\t': //tab
            sb.Append("\\t");
            break;
          case '\n': //line feed
            sb.Append("\\n");
            break;
          case '\f': //form feed
            sb.Append("\\f");
            break;
          case '\r': //carriage return
            sb.Append("\\r");
            break;
          default:
            if (c < ' ')
            {
              String hex = "000" + ((int)c).ToString("X");
              sb.Append("\\u" + hex.Substring(hex.Length - 4));
            }
            else
            {
              sb.Append(c);
            }
            break;
        }
      }
      return sb.ToString();
    }

    private static void RemoveTrailingComma(int indent, StringBuilder builder, Formatting formatting)
    {
      if (formatting.HasFlag(Formatting.Indented))
      {
        if (builder[builder.Length - 4] == ',' && builder[builder.Length - 3] == ' ')
        {
          builder.Remove(builder.Length - 4, 2);
        }
        else if (builder[builder.Length - 2] == ',' && builder[builder.Length - 1] == ' ')
        {
          builder.Remove(builder.Length - 2, 2);
          builder.Append("\r\n");
        }
        else
        {
          builder.Remove(builder.Length - 1, 1);
        }
        builder.Append(' ', indent * 2);
      }
      else
      {
        builder.Remove(builder.Length - 1, 1);
      }
    }

    private static void AppendIndented(int indent, string name, string value, bool isStringValue, StringBuilder builder, bool addComma = true, bool newline = true)
    {
      if (indent > 0)
      {
        builder.Append(' ', indent * 2);
      }
      builder.Append
      (
        String.Format
        (
          CultureInfo.InvariantCulture, "{0}{1}{2}{3}",
          !String.IsNullOrEmpty(name) ? String.Format(CultureInfo.InvariantCulture, indent > 0 ? "\"{0}\": " : "\"{0}\":", name) : String.Empty, //{0}
          isStringValue ? String.Format(CultureInfo.InvariantCulture, "\"{0}\"", value ?? String.Empty)
                        : String.Format(CultureInfo.InvariantCulture, "{0}", value ?? String.Empty), //{1}
          addComma ? (newline ? ", " : ",") : String.Empty, //{2}
          newline ? "\r\n" : String.Empty //{3}
        )
      );
    }
  }
}